/*
 * Copyright (c) 2018 Hitevision
 */

package tw.com.hitevision.whiteboard.android.render;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

import androidx.annotation.ColorInt;
import androidx.core.content.ContextCompat;

import com.hht.wbjni.WbJniCall;

import tw.com.hitevision.whiteboard.android.Props;
import tw.com.hitevision.whiteboard.android.R;


public class Line extends BaseItem {
    private static final String TAG = Line.class.getSimpleName();

    private static float MAX_FILTER_DISTANCE_SQUARED = 100.0f;
    //    private static final int DIFF_DIVIDER = 5; // increase value for greater smoothness
    private static final int DIFF_DIVIDER = 2; // increase value for greater smoothness
    private static final int REGION_MAX_POINT_SIZE = 100;
    //    private static final int DETECT_OFFSET = 2;
//    private static final int SELECTED_EXTEND = 5;
    private int SELECTED_EXTEND;
    private static float PADDING;
    private static final float DEFAULT_POINT_MIN_DISTANCE = 1.0f;
    private static final int DEFAULT_MIN_DISTANCE_DIVISOR = 5;
    private float POINT_DISTANCE = 1.0f;

    List<DiffPoint> points;
    List<Region> regions;
    List<Path> paths;
    final Path path;
    /** Line width. */
    float thickness;
    @ColorInt
    private final int selectedOutlineColor;
    private float selectedThickness;
    private float radius;
    private float extend;

    Path detectPath;
    private float filterDistanceSquared;
    boolean needUpdatePath;
    private Context context;
    float startX;
    float startY;

    final RectF detectRectF;
    final Rect detectRect;
    final Region detectRegion;
    final Region detectClipRegion;
    private Region region;
    private final Matrix mat;
    private final PointF p;
    private final PointF np;
    private final float[] arrayF;

    int lastPointsSize = 0;
    boolean isNeedToUpdateRegion = true;

    private final float originalThickness;
    private float realThickness;
    private final float thicknessOne;

    public Line(Context context, float x, float y, float thickness) {
        points = new ArrayList<>();
        regions = new ArrayList<>();
        paths = new ArrayList<>();
        selectedOutlineColor = ContextCompat.getColor(context, R.color.selection_highlight);
        path = new Path();
        detectPath = new Path();
        path.moveTo(x, y);

        startX = x;
        startY = y;
        this.originalThickness = thickness;
        this.thicknessOne = context.getResources().getDimensionPixelSize(R.dimen.pen_thickness_one);
        realThickness = thickness;
        this.thickness = thickness;
        this.context = context;
        setFilterDistance(thickness);

        detectRectF = new RectF();
        detectRect = new Rect();
        detectRegion = new Region();
        detectClipRegion = new Region();
        region = new Region();

        mat = new Matrix();
        p = new PointF();
        np = new PointF();
        arrayF = new float[2];

        PADDING = context.getResources().getDimensionPixelSize(R.dimen.pen_stroke_padding);
        SELECTED_EXTEND = context.getResources().getDimensionPixelOffset(R.dimen.selected_line_extended_thickness);

        selectedThickness = thickness + SELECTED_EXTEND;
        extend = thickness / 2 + PADDING;
        radius = thickness / 2;

        add(x, y);
    }

    private void updateStrokeWidth(float ratio) {
        float oldRealThickness = realThickness;
        realThickness = oldRealThickness * ratio;

        if (Double.isNaN(realThickness) || Double.isNaN(realThickness) || Double.isInfinite(realThickness) || Double.isInfinite(realThickness)) {
            realThickness = oldRealThickness;
        }

        thickness = Math.max(realThickness, thicknessOne);

        selectedThickness = thickness + SELECTED_EXTEND;
        extend = thickness / 2 + PADDING;
        radius = thickness / 2;
    }

    @Override
    public BaseItem copy() {
        log("test BaseItem copy");
        Line temp = new Line(context, startX, startY, thickness);
        temp.clear();
        temp.path.set(getPath());

        for (int i = 0; i < points.size(); i++) {
            DiffPoint tempDP = new DiffPoint(points.get(i).x, points.get(i).y);
            tempDP.dx = points.get(i).dx;
            tempDP.dy = points.get(i).dy;
            temp.points.add(tempDP);
        }
        temp.x = x;
        temp.y = y;
        temp.width = width;
        temp.height = height;
        temp.setColor(getColor());
        temp.updateDetectPath();
        temp.updatePathAgain = true;

        return temp;
    }

    public static BaseItem copyFromStateFile(Line temp, float startX, float startY, float width, float height, @ColorInt int color, List<DiffPoint> points) {
        log("test BaseItem copyFromStateFile");
        if (points == null || points.size() == 0) {
            return null;
        }

        temp.clear();
        if (points.size() == 1) {
            temp.add(points.get(0).x, points.get(0).y);
        } else {
            temp.points = points;
            temp.updatePath();
        }

        temp.x = startX;
        temp.y = startY;
        temp.width = width;
        temp.height = height;
        temp.setColor(color);
        temp.lastPointsSize = 0;
        temp.updateDetectPath();

        return temp;
    }

    public float getThickness() {
        return thickness;
    }

    private void setFilterDistance(float thickness) {
        filterDistanceSquared = Math.max(thickness * 1.0f, 1.0f);

        if (filterDistanceSquared > MAX_FILTER_DISTANCE_SQUARED) {
            filterDistanceSquared = MAX_FILTER_DISTANCE_SQUARED;
        }

//        filterDistanceSquared = 4;
    }

    public float getFilterDistance() {
        setFilterDistance(thickness);

        return (float) Math.sqrt(filterDistanceSquared);
    }

    public void add(float x, float y) {
        if (points.isEmpty() || allowPoint(x, y)) {
            final DiffPoint point = new DiffPoint(x, y);
            points.add(point);

            if (points.size() == 1) {
                setBounds(x - extend, y - extend, 2 * extend, 2 * extend);
            } else {
                updateBounds(x, y);
                final DiffPoint prev = points.get(points.size() - 2);
                prev.dx = (point.x - prev.x) / DIFF_DIVIDER;
                prev.dy = (point.y - prev.y) / DIFF_DIVIDER;
            }
        }
        needUpdatePath = true;
    }

    private void updateBounds(float x, float y) {
        float left = getLeft();
        float right = getRight();
        float top = getTop();
        float bottom = getBottom();

        left = Math.min(x - extend, left);
        right = Math.max(x + extend, right);
        top = Math.min(y - extend, top);
        bottom = Math.max(y + extend, bottom);

        setBounds(left, top, right - left, bottom - top);
    }

    private void updateActedBounds() {
        if (points.size() > 1) {
            path.computeBounds(detectRectF, true);
            x = detectRectF.left - extend;
            y = detectRectF.top - extend;
            width = detectRectF.right - x + extend;
            height = detectRectF.bottom - y + extend;
        } else {
            setBounds(points.get(0).x - extend, points.get(0).y - extend, 2 * extend, 2 * extend);
        }
    }

    void updateDetectPath() {
        if (detectPath.isEmpty()){
            detectPath.addCircle(points.get(0).x,points.get(0).y,radius, Path.Direction.CCW);
        }

        if (points.size() > 1) {

            for (int i = lastPointsSize==0?0:lastPointsSize-1; i < points.size()-1; i++) {
                PointF diffP = new PointF();
                PointF nDiffP = new PointF();
                diffP.set(points.get(i+1).x-points.get(i).x,points.get(i+1).y-points.get(i).y);
                nDiffP.set(-diffP.y,diffP.x);
                float length = diffP.length();
                //draw a rectangle
                detectPath.moveTo(points.get(i).x+ ((nDiffP.x*radius) / length),points.get(i).y+ ((nDiffP.y*radius) / length));
                detectPath.lineTo(points.get(i+1).x+ ((nDiffP.x*radius) / length),points.get(i+1).y+ ((nDiffP.y*radius) / length));
                detectPath.lineTo(points.get(i+1).x- ((nDiffP.x*radius) / length),points.get(i+1).y- ((nDiffP.y*radius) / length));
                detectPath.lineTo(points.get(i).x- ((nDiffP.x*radius) / length),points.get(i).y- ((nDiffP.y*radius) / length));
                detectPath.close();
                // split long line to small one
                if(i % REGION_MAX_POINT_SIZE == 0&&i != 0){
                    updateDetectRegions();
                    detectPath.rewind();
                }
                detectPath.addCircle(points.get(i+1).x,points.get(i+1).y,radius, Path.Direction.CCW);
            }
        }
        isNeedToUpdateRegion = true;
    }

    public void updateDetectRegions(){
        detectPath.computeBounds(detectRectF, true);

        if(points.isEmpty())
            return;
        // ensure minimum size
        if (detectRectF.width() < 2 * extend && detectRectF.height() < 2 * extend) {
            detectRectF.set(points.get(0).x - extend, points.get(0).y - extend,
                    points.get(0).x + extend, points.get(0).y + extend);

            detectRectF.roundOut(detectRect);
            detectClipRegion.set(detectRect);
            detectPath.addCircle(points.get(0).x, points.get(0).y, radius, Path.Direction.CCW);
            detectRegion.setPath(detectPath, detectClipRegion);
        } else {
            detectRectF.roundOut(detectRect);
            detectClipRegion.set(detectRect);
            detectRegion.setPath(detectPath, detectClipRegion);
        }
        Region subRegion = new Region(detectRegion);
        regions.add(subRegion);
        paths.add(detectPath);
        isNeedToUpdateRegion = false;
    }

    public List<Region> getDetectRegions(){
        if(isNeedToUpdateRegion){
            updateDetectRegions();
        }
        return regions;
    }

    public boolean isEmpty() {
        return points.isEmpty();
    }

    public void clear() {
        regions.clear();
        paths.clear();
        points.clear();
        isNeedToUpdateRegion = true;
        setBounds(0, 0, 0, 0);
        detectRectF.setEmpty();
        detectRegion.setEmpty();
        region.setEmpty();
        detectPath.rewind();
        path.rewind();
        lastPointsSize = 0;
    }

    private boolean allowPoint(float x, float y) {
        if(points.size()<1)
            return true;

        final DiffPoint lastPoint = points.get(points.size() - 1);
        final float dx = x - lastPoint.x;
        final float dy = y - lastPoint.y;
        return (dx * dx + dy * dy) > filterDistanceSquared;
    }

    protected boolean updatePathAgain;
    public void updatePath() {
        if (updatePathAgain && points != null && points.size() > 0) {
             // updatePathAgain为true时重新更新path
            updatePathAgain = false;
            path.reset();
            lastPointsSize = 0;
            path.moveTo(points.get(0).x, points.get(0).y);
        }

        needUpdatePath = false;
        final int size = points.size();
        if (lastPointsSize == size) {
            return;
        }

        // calculate point deltas
        for (int i = (lastPointsSize == 0) ? 0 : lastPointsSize - 1; i < size - 1; i++) {
            DiffPoint point = points.get(i);
            DiffPoint next = points.get(i + 1);

            point.dx = (next.x - point.x) / DIFF_DIVIDER;
            point.dy = (next.y - point.y) / DIFF_DIVIDER;
        }

        // set path
        if(points.size()==2){
//            path.moveTo(points.get(0).x, points.get(0).y);
            path.lineTo((points.get(1).x+points.get(0).x)/2, (points.get(1).y+points.get(0).y)/2);
        }else if(points.size()>2){
//            if(lastPointsSize>0)
//                path.moveTo((points.get(lastPointsSize-1).x + points.get(lastPointsSize).x) / 2,
//                    (points.get(lastPointsSize-1).y + points.get(lastPointsSize).y) / 2);
            for(int i=lastPointsSize; i<points.size()-1; i++) {
                path.quadTo(points.get(i).x, points.get(i).y,
                        (points.get(i).x + points.get(i + 1).x) / 2,
                        (points.get(i).y + points.get(i + 1).y) / 2);
            }
        }

//        final DiffPoint first = points.get(lastPointsSize < 2?0:lastPointsSize-2);
//        path.moveTo(first.x, first.y);
//        for (int i = lastPointsSize < 2?0:lastPointsSize-2; i < size; i++) {
//            final DiffPoint p1 = points.get(i);
//            path.quadTo(p1.x, p1.y, p1.x + p1.dx, p1.y + p1.dy);
//        }



//        if(lastPointsSize==1)
//            path.moveTo(points.get(0).x, points.get(0).y);
//        else if(lastPointsSize==2) {
//            path.lineTo((points.get(0).x+points.get(1).x)/2, (points.get(0).y+points.get(1).y)/2);
//        }else{
//            for(int i=1; i<lastPointsSize; i++){
//                path.quadTo(points.get(lastPointsSize-i-1).x,points.get(lastPointsSize-i-1).y,
//                        (points.get(lastPointsSize-i).x+points.get(lastPointsSize-i-1).x)/2,
//                        (points.get(lastPointsSize-i).y+points.get(lastPointsSize-i-1).y)/2);
//            }
//        }


        updateDetectPath();
        lastPointsSize = points.size();
    }

    public void updatePathForSeletor() {
        WbJniCall.fbStart();
        needUpdatePath = false;
        final int size = points.size();
        if (lastPointsSize == size) {
            return;
        }

        // calculate point deltas
        for (int i = (lastPointsSize == 0) ? 0 : lastPointsSize - 1; i < size - 1; i++) {
            DiffPoint point = points.get(i);
            DiffPoint next = points.get(i + 1);

            point.dx = (next.x - point.x) / DIFF_DIVIDER;
            point.dy = (next.y - point.y) / DIFF_DIVIDER;
        }

        // set path
        final DiffPoint first = points.get(lastPointsSize < 2?0:lastPointsSize-2);
        path.moveTo(first.x, first.y);
        WbJniCall.fbSetPaintColor(getColor(),19);
        WbJniCall.fbSetPaintStrokeWidth(4, 19);
        WbJniCall.fbMoveTo(first.x,first.y,0,19);
        for (int i = lastPointsSize < 2?0:lastPointsSize-2; i < size; i++) {
            final DiffPoint p1 = points.get(i);
            path.quadTo(p1.x, p1.y, p1.x + p1.dx, p1.y + p1.dy);
            WbJniCall.fbQuadto(p1.x, p1.y, p1.x + p1.dx, p1.y + p1.dy,0,19);
        }
        DiffPoint last = points.get(size - 1);
//        path.lineTo(last.x, last.y);
//        WbJniCall.fbLineTo(last.x,last.y,0,19);

        updateDetectPath();
        lastPointsSize = points.size();
    }

    @Override
    public void scaleFromPoint(float rateX, float rateY, PointF p) {
        mat.setScale(rateX, rateY, p.x, p.y);
        transformLine(mat);
        if (rateX == rateY) {
            updateStrokeWidth(Math.min(Math.abs(rateX), Math.abs(rateY)));
        }
        updateActedBounds();
    }

    @Override
    public void rotateFromPoint(float degree, PointF p) {
        mat.setRotate(degree, p.x, p.y);
        transformLine(mat);
        updateActedBounds();
    }

    @Override
    public void moveBy(float dx, float dy) {
        if (dx != 0 || dy != 0) {
            mat.setTranslate(dx, dy);
            transformLine(mat);
            updateActedBounds();
        }
    }

    @Override
    public boolean hit(float x, float y) {
        if(isNeedToUpdateRegion){
            updateDetectRegions();
        }
        for (int i = 0; i < regions.size(); i++) {
            if(regions.get(i).contains((int) x, (int) y))
                return true;
        }
        return false;
    }

    //if regions too long , it may cause crash while using region.op function
    @Override
    public void getRegion(Region out) {
        //todo: need to check this flag to improve speed
//        if(isNeedToUpdateRegion){
            updateDetectRegions();
            for (int i = 0; i < regions.size(); i++) {
                region.op(regions.get(i),Region.Op.UNION);
            }
//        }

        out.set(region);
    }


    @Override
    public void render(Canvas canvas, Paint paint, Context context) {
        if (points.isEmpty()) {
            return;
        }

        if (points.size() == 1) {
            // draw a point
            final DiffPoint point = points.get(0);

            paint.setStyle(Paint.Style.FILL);

            if (isSelected() && !Props.getFlavorModel().equals(Props.Flavor.Model.Oem)) {
                paint.setColor(selectedOutlineColor);
                canvas.drawCircle(point.x, point.y, selectedThickness / 2, paint);
            }

            paint.setColor(getColor());
            canvas.drawCircle(point.x, point.y, thickness / 2, paint);
            return;
        }

        if (needUpdatePath) {
            updatePath();
        }

        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeCap(Paint.Cap.ROUND);
        paint.setStrokeJoin(Paint.Join.ROUND);

        if (isSelected() && !Props.getFlavorModel().equals(Props.Flavor.Model.Oem)) {
            // draw selected outline
            paint.setStrokeWidth(selectedThickness);
            paint.setColor(selectedOutlineColor);
            canvas.drawPath(path, paint);
        }

        paint.setColor(getColor());
        paint.setStrokeWidth(thickness);
        canvas.drawPath(path, paint);

//        Paint p = new Paint();
//        p.setColor(-1929859042);
//
//        canvas.drawPath(detectPath,p);
    }

    private void transformLine(Matrix mat){
        applyMoveToPoints(mat);
        path.transform(mat);
        regions.clear();
        detectPath.rewind();
        for (int i = 0; i < paths.size(); i++) {
            paths.get(i).transform(mat);
        }
        lastPointsSize = 0;
        updateDetectPath();
    }

    private void applyMoveToPoints(Matrix mat) {
        float[] temp = arrayF;
        for (int i = 0; i < points.size(); i++) {
            temp[0] = points.get(i).x;
            temp[1] = points.get(i).y;
            mat.mapPoints(temp);
            points.get(i).set(temp[0], temp[1]);
        }
    }

    public Path getPath() {
        return path;
    }

    public List<DiffPoint> getPoints(){
        return points;
    }

    public List<BaseItem> splitLine(Line eraserLine) {

        int multiple = points.size()/ REGION_MAX_POINT_SIZE / DEFAULT_MIN_DISTANCE_DIVISOR;
        if (multiple == 0) {
            multiple = 1;
        } else if (multiple > 5) {
            multiple = 5;
        }
        POINT_DISTANCE = DEFAULT_POINT_MIN_DISTANCE * multiple;
        boolean isNeedCreateSubLine = true;
        List<BaseItem> sliptLineList = new ArrayList<>();
        Line subLine = null;
        Line checkLine = null;
        PointF newStartPoint = points.get(0);
        PointF intersectPoint = null;

        for (int i = 0; i < points.size(); i++) {
            if(checkLine == null){
                checkLine = newFromSelf(points.get(i).x,points.get(i).y);
            }else{
                checkLine.add(points.get(i).x,points.get(i).y);
                checkLine.updatePath();
            }
            if (checkInterSect(eraserLine,checkLine)) {
                if (!isNeedCreateSubLine) {
                    intersectPoint = getIntersectPointF(points.get(i - 1),points.get(i),eraserLine);
                    subLine.add(intersectPoint.x,intersectPoint.y);
                    sliptLineList.add(subLine);
                    isNeedCreateSubLine = true;
                }
                checkLine = null;
            }else{
                if (isNeedCreateSubLine) {
                    if (i > 0) {
                        newStartPoint = getDisjointF(points.get(i - 1), points.get(i), eraserLine);
                        if (newStartPoint == null) {
                            if(i >1){
                                newStartPoint = getDisjointF(points.get(i - 2), points.get(i - 1), eraserLine);
                            }
                        }
                    }
                    if(newStartPoint == null){
                        if(i<3){
                            newStartPoint = points.get(0);
                        }else{
                            newStartPoint = points.get(i-2);
                        }
                    }
                    subLine = newFromSelf(newStartPoint.x, newStartPoint.y);
                    isNeedCreateSubLine = false;
                }
                subLine.add(points.get(i).x,points.get(i).y);
                checkLine.clear();
                checkLine.add(points.get(i).x,points.get(i).y);
            }
        }

        if(!isNeedCreateSubLine){
            sliptLineList.add(subLine);
        }

//        for (BaseItem item : sliptLineList) {
//            ((Line) item).updatePath();
//            if(followingStickynoteId!=0L){
//                ((Line) item).setFollowingStickynoteId(followingStickynoteId);
//            }
//        }

        return sliptLineList;
    }

    private PointF getIntersectPointF(PointF start,PointF end,Line eraserLine){
        Line line = newFromSelf(start.x,start.y);
        PointF intersectPointF = null;
        float differenceX = end.x-start.x;
        float differenceY = end.y-start.y;
        double pointDistance = Math.sqrt((differenceX*differenceX)+(differenceY*differenceY));
        float i = POINT_DISTANCE;
        while(i<pointDistance){
            PointF addPointF = new PointF((float) (start.x + differenceX*i/pointDistance), (float) (start.y +differenceY*i/pointDistance));
            line.add(addPointF.x, addPointF.y);
            line.updatePath();
            if(checkInterSect(eraserLine,line)){
                intersectPointF = new PointF(addPointF.x, addPointF.y);
                return intersectPointF;
            }
            i+= POINT_DISTANCE;
        }
        intersectPointF = new PointF(end.x,end.y);
        return intersectPointF;
    }

    private PointF getDisjointF(PointF start,PointF end,Line eraserLine){
        Line line = newFromSelf(end.x,end.y);
        PointF disjointF = null;
        float differenceX = end.x-start.x;
        float differenceY = end.y-start.y;
        double pointDistance = Math.sqrt((differenceX*differenceX)+(differenceY*differenceY));
        double i = POINT_DISTANCE;
        while(i<pointDistance){
            PointF addPointF = new PointF((float) (end.x - differenceX * i / pointDistance), (float) (end.y - differenceY * i / pointDistance));
            line.add(addPointF.x,addPointF.y);
            line.updatePath();
            if(checkInterSect(eraserLine,line)){
                disjointF = new PointF(addPointF.x,addPointF.y);
                return disjointF;
            }
            i+= POINT_DISTANCE;
        }
        return null;
    }

    private Line newFromSelf(float startX,float startY){
        if(this instanceof Highlighter){
            Highlighter temp = new Highlighter(context, startX, startY, thickness);
            temp.setColor(getColor());
            temp.updatePath();
            return temp;
        }else{
            Pen temp = new Pen(context, startX, startY, thickness);
            temp.setColor(getColor());
            temp.updatePath();
            return temp;
        }
    }

    //this function is for regions to regions check
    private boolean checkInterSect(Line line1,Line line2){
        for (int i = 0; i < line1.getDetectRegions().size(); i++) {
            Region subRegion1 = new Region(line1.getDetectRegions().get(i));
            for (int j = 0; j < line2.getDetectRegions().size(); j++) {
                Region subRegion2 = new Region(line2.getDetectRegions().get(j));
                if(subRegion2.op(subRegion1,Region.Op.INTERSECT)){
                    return true;
                }
            }
        }
        return false;
    }

    private boolean checkInterSect(Line line,Region region){
        for (int i = 0; i < line.getDetectRegions().size(); i++) {
            Region subRegion = new Region(line.getDetectRegions().get(i));
            if (subRegion.op(region, Region.Op.INTERSECT)) {
                return true;
            }
        }
        return false;
    }

    public static class DiffPoint extends PointF {
        float dx;
        float dy;

        DiffPoint(float x, float y) {
            super(x, y);
        }

    }

    public void updateAll() {
        updatePath();
        updateDetectRegions();
    }

//    public Long getFollowingStickynoteId(){
//        return followingStickynoteId;
//    }
//
//    public void setFollowingStickynoteId(Long id) {
//        followingStickynoteId = id;
//    }

    private static void log(String msg) {
        Log.d(TAG, msg);
    }
}
